/*******************************************************************************
 * Copyright (c) 2001, 2003 Boris Pruessmann and others. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Common Public License v0.5 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/cpl-v05.html
 *
 * Contributors: Boris Pruessmann - Initial implementation David Corbin - added
 * Revert action (now RevertFromChangelistAction)
 ******************************************************************************/
package net.sourceforge.perforce.ui.views;

import com.google.common.collect.Maps;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import net.sourceforge.perforce.core.IChangelistListener;
import net.sourceforge.perforce.core.IResourceStateChangeListener;
import net.sourceforge.perforce.core.PerforceCorePlugin;
import net.sourceforge.perforce.core.PerforceException;
import net.sourceforge.perforce.core.ResourceStateChangeAdapter;
import net.sourceforge.perforce.core.api.IPerforceListener;
import net.sourceforge.perforce.core.api.IPerforceServer;
import net.sourceforge.perforce.core.api.IPerforceServerManager;
import net.sourceforge.perforce.core.resources.IChangelist;
import net.sourceforge.perforce.core.resources.IP4File;
import net.sourceforge.perforce.core.syncinfo.IResourceStateDelta;
import net.sourceforge.perforce.ui.IPerforceUIConstants;
import net.sourceforge.perforce.ui.PerforceUIPlugin;
import net.sourceforge.perforce.ui.internal.Policy;
import net.sourceforge.perforce.ui.internal.dnd.LocalFileTransfer;
import net.sourceforge.perforce.ui.internal.dnd.PendingChangelistDragAdapter;
import net.sourceforge.perforce.ui.internal.dnd.PendingChangelistDropAdapter;
import net.sourceforge.perforce.ui.internal.model.ClientPendingChangelistContainer;
import net.sourceforge.perforce.ui.internal.model.OtherPendingChangelistContainer;
import net.sourceforge.perforce.ui.internal.model.PendingChangelistCache;
import net.sourceforge.perforce.ui.model.AllRootsElement;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.viewers.DecoratingLabelProvider;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IColorProvider;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerSorter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.TreeEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.model.WorkbenchContentProvider;
import org.eclipse.ui.model.WorkbenchLabelProvider;
import org.eclipse.ui.part.DrillDownAdapter;
import org.eclipse.ui.part.ViewPart;

/**
 * The PendingChangelistView defines a view for managing Perforce changelists.
 * Compare with P4Win.
 *
 * @version $Revision: 494 $
 * @author <a href="mailto:bpruessmann@users.sourceforge.net">Boris
 *         Pruessmann</a>
 * @author <a href="mailto:dcorbin@users.sourceforge.net">David Corbin</a>
 */
public class PendingChangelistView extends ViewPart implements IPerforceListener,
    IChangelistListener {
  //----------------------------------------------------------------------------
  // Public Constants

  /** The view's identifier. */
  public static final String ID = PerforceUIPlugin.ID + ".views.PendingChangelistView"; //$NON-NLS-1$

  // --------------------------------------------------------------------
  // Private Static Variables

  /** Stores the view's singleton instance. */
  private static PendingChangelistView instance;

  // ---------------------------------------------------------------------------
  // Private Variables

  /** Holds the view's root element. */
  private AllRootsElement root;
  /** Stores the tree viewer used to display the pending changelist tree. */
  private InternalTreeViewer viewer;
  /** Stores the DrillDown adapter. */
  private DrillDownAdapter drillPart;
  /** Stores the RefreshAction. */
  private RefreshAction refreshAction;

  /** Resource State Change Listener. */
  private IResourceStateChangeListener changeListener;
  /** Changelist listener. */
  private IChangelistListener changelistListener;

  /** Stores the foreground color to use for the active changelist. */
  private Color foregroundColor;

  // --------------------------------------------------------------------
  // Private Static Variables

  /** Stores the Pending Changelist Caches for the servers. */
  private static Map<IPerforceServer, PendingChangelistCache> pendingChangelistCaches =
      Maps.newHashMap();

  // -----------------------------------------------------------------------
  // Public Static Methods

  /**
   * Call this method to retrieve the PendingChangeListView singleton.
   *
   * @return the singleton.
   */
  public static final PendingChangelistView getInstance() {
    return instance;
  }

  /**
   * Called to retrieve the PendingChangelistCache for the given server.
   *
   * @param server the server.
   * @return the server's PendingChangelistCache.
   */
  public static PendingChangelistCache getPendingChangelistCache(IPerforceServer server) {
    PendingChangelistCache result = null;

    synchronized (pendingChangelistCaches) {
      result = pendingChangelistCaches.get(server);
      if (result == null) {
        result = new PendingChangelistCache(server);
        pendingChangelistCaches.put(server, result);
      }
    }

    return result;
  }

  // -------------------------------------------------------------------------
  // Public Constructors

  /** Constructor for PendingChangelistView. */
  public PendingChangelistView() {
    instance = this;
    PerforceCorePlugin.getPlugin().addChangelistListener(this);
  }

  //----------------------------------------------------------------------------
  // -- Public Methods

  @Override
  public void createPartControl(Composite parent) {
    root = new AllRootsElement();

    ILabelProvider labelProvider = getLabelProvider();

    viewer = new InternalTreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
    viewer.setContentProvider(new ContentProvider());
    viewer.setLabelProvider(labelProvider);
    viewer.setSorter(new InternalSorter());
    viewer.setInput(root);
    viewer.getControl().addKeyListener(new KeyAdapter() {
      @Override
      public void keyPressed(KeyEvent event) {
        if (event.keyCode == SWT.F5) {
          refreshAction.run();
        }
      }
    });

    drillPart = new DrillDownAdapter(viewer);

    getSite().setSelectionProvider(viewer);
    contributeActions();
    initDragAndDrop();

    changeListener = new ResourceStateChangeAdapter() {
      @Override
      public void resourceStateChanged(IResourceStateDelta[] resourceStateDeltas) {
        IPerforceServerManager manager = PerforceCorePlugin.getServerManager();

        final Set<IPerforceServer> serverSet = new HashSet<IPerforceServer>();
        for (int i = 0; i < resourceStateDeltas.length; i++) {
          try {
            IResource resource = resourceStateDeltas[i].getResource();
            IPerforceServer server = manager.getPerforceServer(resource);
            serverSet.add(server);
          } catch (PerforceException e) {
            PerforceUIPlugin.log(e.getStatus());
          }
        }


        Shell shell = getSite().getShell();
        shell.getDisplay().asyncExec(new Runnable() {
          public void run() {
            for (Iterator<IPerforceServer> it = serverSet.iterator(); it.hasNext();) {
              internalRefresh(it.next());
            }
          }
        });
      }

      @Override
      public void changelistStateChanged(IChangelist[] changelists) {
        final Set<IPerforceServer> serverSet = new HashSet<IPerforceServer>();
        for (int i = 0; i < changelists.length; i++) {
          serverSet.add(changelists[i].getServer());
        }

        Shell shell = getSite().getShell();
        shell.getDisplay().asyncExec(new Runnable() {
          public void run() {
            for (Iterator<IPerforceServer> it = serverSet.iterator(); it.hasNext();) {
              internalRefresh(it.next());
            }
          }
        });
      }

      @Override
      public void changelistStateChanged(final IPerforceServer[] servers) {
        Shell shell = getSite().getShell();
        shell.getDisplay().asyncExec(new Runnable() {
          public void run() {
            for (int i = 0; i < servers.length; i++) {
              internalRefresh(servers[i]);
            }
          }
        });
      }
    };
    PerforceCorePlugin.getPlugin().addResourceStateChangeListener(changeListener);
    PerforceCorePlugin.getServerManager().addPerforceListener(this);

    changelistListener = new IChangelistListener() {
      public void notifyChange() {
        Shell shell = getSite().getShell();
        shell.getDisplay().asyncExec(new Runnable() {
          public void run() {
            viewer.refresh();
          }
        });
      }
    };
    PerforceCorePlugin.getPlugin().addChangelistListener(changelistListener);

    viewer.setInternalTreeViewerListener(new InternalTreeViewerListenerAdapter() {
      @Override
      public void afterTreeCollapsed(TreeEvent event) {
        Object element = event.item.getData();
        if (element instanceof OtherPendingChangelistContainer) {
          OtherPendingChangelistContainer container = (OtherPendingChangelistContainer) element;

          IPerforceServer server = container.getServer();
          PendingChangelistCache cache = getPendingChangelistCache(server);
          cache.setPerformOpenedAll(false);
        } else if (element instanceof IPerforceServer) {
          IPerforceServer server = (IPerforceServer) element;
          PendingChangelistCache cache = getPendingChangelistCache(server);
          cache.setPerformOpenedAll(false);
        }
      }

      @Override
      public void beforeTreeExpanded(TreeEvent event) {
        Object element = event.item.getData();
        if (element instanceof OtherPendingChangelistContainer) {
          final OtherPendingChangelistContainer container =
              (OtherPendingChangelistContainer) element;

          IPerforceServer server = container.getServer();
          PendingChangelistCache cache = getPendingChangelistCache(server);
          cache.setPerformOpenedAll(true);
          cache.invalidate();
        }
      }
    });
  }

  @Override
  public void setFocus() {
    viewer.getControl().setFocus();
  }

  @Override
  public void dispose() {
    PerforceCorePlugin.getPlugin().removeChangelistListener(changelistListener);
    PerforceCorePlugin.getPlugin().removeResourceStateChangeListener(changeListener);
    PerforceCorePlugin.getServerManager().removePerforceListener(this);

    viewer.getLabelProvider().dispose();

    super.dispose();
    if (foregroundColor != null) {
      foregroundColor.dispose();
      foregroundColor = null;
    }
  }

  // ---------------------------------------------------------- Public Methods
  // (IPerforceListener)

  /** @see IPerforceListener#perforceServerAdded(IPerforceServer) */
  public void perforceServerAdded(IPerforceServer server) {
    Display display = getSite().getShell().getDisplay();
    display.asyncExec(new Runnable() {
      public void run() {
        internalRefresh();
      }
    });
  }

  /** @see IPerforceListener#perforceServerRemoved(IPerforceServer) */
  public void perforceServerRemoved(IPerforceServer server) {
    Display display = getSite().getShell().getDisplay();
    display.asyncExec(new Runnable() {
      public void run() {
        internalRefresh();
      }
    });
  }

  //----------------------------------------------------------------------------
  // - Private Methods

  /** Called to refresh the view. Invaliadtes all PendingChangelistCaches. */
  private void internalRefresh() {
    for (Iterator<PendingChangelistCache> it = pendingChangelistCaches.values().iterator(); it
        .hasNext();) {
      PendingChangelistCache cache = it.next();
      cache.invalidate();
    }

    viewer.refresh();
  }

  /**
   * Called to refresh the given view element. Invalidates the responsible
   * PendingChangelistCache.
   *
   * @param o the object to refresh.
   */
  private void internalRefresh(Object o) {
    IPerforceServer server = null;
    if (o instanceof IPerforceServer) {
      server = (IPerforceServer) o;
    } else if (o instanceof IChangelist) {
      server = ((IChangelist) o).getServer();
    } else if (o instanceof IP4File) {
      server = ((IP4File) o).getServer();
    }

    if (server != null) {
      PendingChangelistCache cache = pendingChangelistCaches.get(server);
      cache.invalidate();
    }

    viewer.refresh(o);
  }

  /** Adds drag and drop support to the pending changelists view. */
  private void initDragAndDrop() {
    int operations = DND.DROP_MOVE;
    Transfer[] transfers = new Transfer[] {LocalFileTransfer.getInstance()};

    PendingChangelistDragAdapter dragAdapter = new PendingChangelistDragAdapter(viewer);
    viewer.addDragSupport(operations, transfers, dragAdapter);

    PendingChangelistDropAdapter dropAdapter = new PendingChangelistDropAdapter(viewer);
    dropAdapter.setFeedbackEnabled(false);
    viewer.addDropSupport(operations, transfers, dropAdapter);
  }

  /**
   * Contribute actions to the view.
   */
  private void contributeActions() {
    viewer.addDoubleClickListener(new DoubleClickListener());

    refreshAction = new RefreshAction();

    // Create the popup menu
    MenuManager menuMgr = new MenuManager();
    Tree tree = viewer.getTree();
    Menu menu = menuMgr.createContextMenu(tree);
    menuMgr.addMenuListener(new IMenuListener() {
      public void menuAboutToShow(IMenuManager manager) {
        manager.add(new Separator("perforce.group1"));
        manager.add(new Separator(IWorkbenchActionConstants.SEP));
        manager.add(new Separator("perforce.group2"));
        manager.add(new Separator(IWorkbenchActionConstants.SEP));
        manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
        manager.add(refreshAction);
      }
    });
    menuMgr.setRemoveAllWhenShown(true);
    tree.setMenu(menu);
    getSite().registerContextMenu(menuMgr, viewer);

    // Create the local tool bar
    IActionBars bars = getViewSite().getActionBars();
    IToolBarManager tbm = bars.getToolBarManager();
    drillPart.addNavigationActions(tbm);
    tbm.add(refreshAction);
    tbm.update(false);
  }

  /**
   * Called to retrieve the LabelProvider that is used.
   *
   * @return the LabelProvider.
   */
  private ILabelProvider getLabelProvider() {
    return new ColoredWorkbenchLabelProvider(new DecoratingLabelProvider(
        new WorkbenchLabelProvider(), new ChangelistViewDecorator()));
  }

  // -----------------------------------------------------------------------
  // Private Inner Classes

  /**
   * Handler class for double clicks.
   *
   * TODO: (re-) add double click of LocalFile.
   */
  private class DoubleClickListener implements IDoubleClickListener {
    /** @see IDoubleClickListener#doubleClick(DoubleClickEvent) */
    public void doubleClick(DoubleClickEvent event) {
      IStructuredSelection selection = (IStructuredSelection) viewer.getSelection();
      IAdaptable element = (IAdaptable) selection.getFirstElement();

      if (element instanceof IP4File) {
        doubleClickLocalFile((IP4File) element);
      } else {
        viewer.expandToLevel(element, 1);
      }
    }

    /**
     * Performs handling of double clicks on LocalFile elements.
     *
     * @param localFile the LocalFile instance
     */
    private void doubleClickLocalFile(IP4File localFile) {
      IWorkspaceRoot root = PerforceUIPlugin.getWorkspace().getRoot();
      IFile file = root.getFileForLocation(localFile.getLocation());
      if (file == null || !file.isAccessible()) {
        return;
      }

      PerforceUIPlugin.openEditor(file);
    }

  }
  /** Action class for refreshing the view. */
  private class RefreshAction extends Action implements Runnable {
    //--------------------------------------------------------------------------
    // -- Constructors

    /** Constructor for RefreshAction. */
    RefreshAction() {
      super(Policy.bind("PendingChangelistView.refreshAction.label")); //$NON-NLS-1$

      setImageDescriptor(PerforceUIPlugin.getPlugin().getImageDescriptor(
          IPerforceUIConstants.IMG_REFRESH));
    }

    //--------------------------------------------------------------------------
    // Public Methods

    @Override
    public void run() {
      internalRefresh();
    }
  }

  /**
   * This class makes the tree view a little "smarter" ;)
   */
  private class ContentProvider extends WorkbenchContentProvider {
    /** Stores a mapping between server and children. */
    private Map<IPerforceServer, Object[]> childMap = new HashMap<IPerforceServer, Object[]>();

    @Override
    public boolean hasChildren(Object element) {
      if (element instanceof ClientPendingChangelistContainer
          || element instanceof OtherPendingChangelistContainer) {
        return true;
      }

      return super.hasChildren(element);
    }

    @Override
    public Object[] getChildren(Object element) {
      if (!(element instanceof IPerforceServer)) {
        return super.getChildren(element);
      }

      IPerforceServer server = (IPerforceServer) element;
      Object[] result = childMap.get(server);
      if (result == null) {
        PendingChangelistCache cache = getPendingChangelistCache(server);
        result =
            new Object[] {new ClientPendingChangelistContainer(server, cache),
                new OtherPendingChangelistContainer(server, cache),};
        childMap.put(server, result);
      }

      return result;
    }
  }

  /**
   * Class responsible for sorting the tree.
   */
  private static class InternalSorter extends ViewerSorter {
    @Override
    public int compare(Viewer viewer, Object e1, Object e2) {
      if (e1 instanceof IPerforceServer && e2 instanceof IPerforceServer) {
        IPerforceServer server1 = (IPerforceServer) e1;
        IPerforceServer server2 = (IPerforceServer) e2;

        return server1.getUniqueName().compareTo(server2.getUniqueName());
      } else if (e1 instanceof IChangelist && e2 instanceof IChangelist) {
        IChangelist changelist1 = (IChangelist) e1;
        IChangelist changelist2 = (IChangelist) e2;

        return changelist1.getId().intValue() - changelist2.getId().intValue();
      } else {
        return super.compare(viewer, e1, e2);
      }
    }
  }

  public void notifyChange() {
    Shell shell = getSite().getShell();
    shell.getDisplay().asyncExec(refreshAction);
  }

  /**
   * LabelProvider that colors the currently active changelist.
   */
  private class ColoredWorkbenchLabelProvider implements ILabelProvider, IColorProvider {
    // -----------------------------------------------------------------------
    // Private Variables

    /** Stores the provider for delegation. */
    private final ILabelProvider delegate;

    // ---------------------------------------------------------------------
    // Public Constructors

    /** Constructor for ColoredWorkbenchLabelProvider. */
    public ColoredWorkbenchLabelProvider(ILabelProvider theDelegate) {
      delegate = theDelegate;

      if (foregroundColor == null) {
        Display display = Display.getCurrent();
        foregroundColor = new Color(display, new RGB(0, 0, 255));
      }
    }

    //--------------------------------------------------------------------------
    // - Public Method

    /** @see IColorProvider#getForeground(java.lang.Object) */
    public Color getForeground(Object element) {
      if (!(element instanceof IChangelist)) {
        return null;
      }

      final IChangelist changelist = (IChangelist) element;
      final IPerforceServer server = changelist.getServer();
      if (server.getActiveChangelistId().equals(changelist.getId())
          && server.getClient().equals(changelist.getClientName())) {
        return foregroundColor;
      } else {
        return null;
      }
    }

    public Color getBackground(Object element) {
      return null;
    }

    public Image getImage(Object element) {
      return delegate.getImage(element);
    }

    public String getText(Object element) {
      return delegate.getText(element);
    }

    public void addListener(ILabelProviderListener listener) {
      delegate.addListener(listener);
    }

    public void dispose() {
      delegate.dispose();
    }

    public boolean isLabelProperty(Object element, String property) {
      return delegate.isLabelProperty(element, property);
    }

    public void removeListener(ILabelProviderListener listener) {
      delegate.removeListener(listener);
    }
  }
}
